﻿using BBDown.Core.Protobuf;
using Google.Protobuf;
using System.Buffers.Binary;
using System.IO.Compression;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
using static BBDown.Core.Logger;

namespace BBDown.Core
{
    class AppHelper
    {
        private static readonly string API = "https://grpc.biliapi.net/bilibili.app.playurl.v1.PlayURL/PlayView";
        private static readonly string API2 = "https://app.bilibili.com/bilibili.pgc.gateway.player.v1.PlayURL/PlayView";
        private static readonly string dalvikVer = "2.1.0";
        private static readonly string osVer = "11";
        private static readonly string brand = "M2012K11AC";
        private static readonly string model = "Build/RKQ1.200826.002";
        private static readonly string appVer = "6.32.0";
        private static readonly int build = 6320200;
        private static readonly string channel = "xiaomi_cn_tv.danmaku.bili_zm20200902";
        private static readonly Network.Types.TYPE networkType = Network.Types.TYPE.Wifi;
        private static readonly string networkOid = "46007";
        private static readonly string cronet = "1.36.1";
        private static readonly string buvid = "";
        private static readonly string mobiApp = "android";
        private static readonly string appKey = "android64";
        private static readonly string sessionId = "dedf8669";
        private static readonly string platform = "android";
        private static readonly string env = "prod";
        private static readonly int appId = 1;
        private static readonly string region = "CN";
        private static readonly string language = "zh";

        /// <summary>
        /// 发起请求并返回响应报文(protobuf -> json)
        /// </summary>
        /// <param name="epId"></param>
        /// <param name="cid"></param>
        /// <param name="qn"></param>
        /// <param name="appkey"></param>
        /// <returns></returns>
        public static async Task<string> DoReqAsync(string aid, string cid, string epId, string qn, bool bangumi, bool onlyAvc, string appkey = "")
        {
            var headers = GetHeader(appkey);
            LogDebug("App-Req-Headers: {0}", JsonSerializer.Serialize(headers, JsonContext.Default.DictionaryStringString));
            var body = GetPayload(Convert.ToInt64(aid), Convert.ToInt64(cid), Convert.ToInt64(qn), onlyAvc ? PlayViewReq.Types.CodeType.Code264 : PlayViewReq.Types.CodeType.Code265);
            //Console.WriteLine(ReadMessage<PlayViewReq>(body));
            var data = await GetPostResponseAsync(API, body, headers);
            PlayViewReply? resp;
            try
            {
                resp = new MessageParser<PlayViewReply>(() => new PlayViewReply()).ParseFrom(ReadMessage(data));
            }
            catch (Exception ex)
            {
                if (Config.DEBUG_LOG)
                {
                    LogError(ex.Message);
                }

                if (bangumi)
                {
                    body = GetPayload(Convert.ToInt64(epId), Convert.ToInt64(cid), Convert.ToInt64(qn), onlyAvc ? PlayViewReq.Types.CodeType.Code264 : PlayViewReq.Types.CodeType.Code265);
                    data = await GetPostResponseAsync(API2, body, headers);
                    resp = new MessageParser<PlayViewReply>(() => new PlayViewReply()).ParseFrom(ReadMessage(data));
                }
                else
                {
                    throw;
                }
            }
            LogDebug("PlayViewReplyPlain: {0}", JsonSerializer.Serialize(resp, JsonContext.Default.PlayViewReply));
            return ConvertToDashJson(resp);
        }

        /// <summary>
        /// 将protobuf转换成网页那种json 这样就不用修改之前的解析逻辑了
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        private static string ConvertToDashJson(object data)
        {
            var resp = (PlayViewReply)data;
            var videos = new List<object>();
            var audios = new List<object>();

            if (resp.VideoInfo.StreamList != null)
            {
                foreach (var item in resp.VideoInfo.StreamList)
                {
                    if (item.DashVideo != null)
                    {
                        videos.Add(new AudioInfoWitCodecId(
                            item.StreamInfo.Quality,
                            item.DashVideo.BaseUrl,
                            (uint)(item.DashVideo.Size * 8 / (resp.VideoInfo.Timelength / 1000)),
                            item.DashVideo.Codecid
                        ));
                    }
                }
            }

            if (resp.VideoInfo.DashAudio != null)
            {
                foreach (var item in resp.VideoInfo.DashAudio)
                {
                    audios.Add(new AudioInfoWithCodecName(
                        item.Id,
                        item.BaseUrl,
                        item.BackupUrl.ToList(),
                        item.Bandwidth,
                        "M4A"
                    ));
                }
            }

            if (resp.VideoInfo.Dolby != null && resp.VideoInfo.Dolby.Audio != null)
            {
                audios.Add(new AudioInfoWithCodecName(
                    resp.VideoInfo.Dolby.Audio.Id,
                    resp.VideoInfo.Dolby.Audio.BaseUrl,
                    resp.VideoInfo.Dolby.Audio.BackupUrl.ToList(),
                    resp.VideoInfo.Dolby.Audio.Bandwidth,
                    "E-AC-3"
                ));
            }

            var json = new DashJson(
                0,
                "0",
                1,
                new DashData(
                    resp.VideoInfo.Timelength,
                    new DashInfo(
                        videos,
                        audios
                    )
                )
            );

            return JsonSerializer.Serialize(json, JsonContext.Default.DashJson);
        }

        private static byte[] GetPayload(long aid, long cid, long qn, PlayViewReq.Types.CodeType codec)
        {
            var obj = new PlayViewReq
            {
                EpId = aid,
                Cid = cid,
                //obj.Qn = qn;
                Qn = 126,
                Fnval = 976,
                Spmid = "main.ugc-video-detail.0.0",
                FromSpmid = "main.my-history.0.0",
                PreferCodecType = codec,
                Download = 0, //0:播放 1:flv下载 2:dash下载
                ForceHost = 2 //0:允许使用ip 1:使用http 2:使用https
            };
            LogDebug("PayLoadPlain: {0}", JsonSerializer.Serialize(obj, JsonContext.Default.PlayViewReq));
            return PackMessage(obj.ToByteArray());
        }


        #region 生成Headers相关方法

        private static Dictionary<string, string> GetHeader(string appkey)
        {
            return new Dictionary<string, string>()
            {
                ["Host"] = "grpc.biliapi.net",
                ["user-agent"] = $"Dalvik/{dalvikVer} (Linux; U; Android {osVer}; {brand} {model}) {appVer} os/android model/{brand} mobi_app/android build/{build} channel/{channel} innerVer/{build} osVer/{osVer} network/2 grpc-java-cronet/{cronet}",
                ["te"] = "trailers",
                ["x-bili-fawkes-req-bin"] = GenerateFawkesReqBin(),
                ["x-bili-metadata-bin"] = GenerateMetadataBin(appkey),
                ["authorization"] = $"identify_v1 {Config.TOKEN}",
                ["x-bili-device-bin"] = GenerateDeviceBin(),
                ["x-bili-network-bin"] = GenerateNetworkBin(),
                ["x-bili-restriction-bin"] = "",
                ["x-bili-locale-bin"] = GenerateLocaleBin(),
                ["x-bili-exps-bin"] = "",
                ["grpc-encoding"] = "gzip",
                ["grpc-accept-encoding"] = "identity,gzip",
                ["grpc-timeout"] = "17996161u",
            };
        }

        private static string GenerateLocaleBin()
        {
            var obj = new Locale
            {
                CLocale = new Locale.Types.LocaleIds
                {
                    Language = language,
                    Region = region
                }
            };
            return Convert.ToBase64String(obj.ToByteArray());
        }

        private static string GenerateNetworkBin()
        {
            var obj = new Network
            {
                Type = networkType,
                Oid = networkOid
            };
            return Convert.ToBase64String(obj.ToByteArray());
        }

        private static string GenerateDeviceBin()
        {
            var obj = new Device
            {
                AppId = appId,
                Build = build,
                Buvid = buvid,
                MobiApp = mobiApp,
                Platform = platform,
                Channel = channel,
                Brand = brand,
                Model = model,
                Osver = osVer
            };
            return Convert.ToBase64String(obj.ToByteArray());
        }

        private static string GenerateMetadataBin(string appkey)
        {
            var obj = new Metadata
            {
                AccessKey = appkey,
                MobiApp = mobiApp,
                Build = build,
                Channel = channel,
                Buvid = buvid,
                Platform = platform
            };
            return Convert.ToBase64String(obj.ToByteArray());
        }

        private static string GenerateFawkesReqBin()
        {
            var obj = new FawkesReq
            {
                Appkey = appKey,
                Env = env,
                SessionId = sessionId
            };
            return Convert.ToBase64String(obj.ToByteArray());
        }

        #endregion

        /// <summary>
        /// 读取gRPC响应流 通过前5字节信息 解析/解压后面的报文体
        /// </summary>
        /// <param name="data"></param>
        /// <returns>字节流</returns>
        private static byte[] ReadMessage(byte[] data)
        {
            byte first;
            int size;
            (first, size) = ReadInfo(data);
            return first == 1 ? GzipDecompress(data[5..]) : data[5..(5 + size)];
        }

        /// <summary>
        /// 读取报文长度
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        private static (byte first, int size) ReadInfo(byte[] data)
        {
            var value1 = data[0];
            var value2 = data[1..5];

            return (value1, BinaryPrimitives.ReadInt32BigEndian(value2));
        }

        /// <summary>
        /// 给请求载荷添加头部信息
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        private static byte[] PackMessage(byte[] input)
        {
            using var stream = new MemoryStream();
            using (var writer = new BinaryWriter(stream))
            {
                var comp = GzipCompress(input);
                var reverse = (stackalloc byte[4]);
                writer.Write((byte)1);
                BinaryPrimitives.WriteInt32BigEndian(reverse, comp.Length);
                writer.Write(reverse);
                writer.Write(comp);
            }
            return stream.ToArray();
        }

        /// <summary>
        /// gzip压缩
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        private static byte[] GzipCompress(byte[] data)
        {
            using var output = new MemoryStream();
            using (var comp = new GZipStream(output, CompressionMode.Compress))
            {
                comp.Write(data, 0, data.Length);
            }
            return output.ToArray();
        }

        /// <summary>
        /// gzip解压
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        private static byte[] GzipDecompress(byte[] data)
        {
            using var output = new MemoryStream();
            using (var input = new MemoryStream(data))
            {
                using var decomp = new GZipStream(input, CompressionMode.Decompress);
                decomp.CopyTo(output);
            }
            return output.ToArray();
        }

        public static async Task<byte[]> GetPostResponseAsync(string Url, byte[] postData, Dictionary<string, string> headers)
        {
            LogDebug("Post to: {0}, data: {1}", Url, Convert.ToBase64String(postData));

            ByteArrayContent content = new(postData);
            content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/grpc");

            HttpRequestMessage request = new()
            {
                RequestUri = new Uri(Url),
                Method = HttpMethod.Post,
                Content = content,
                //Version = HttpVersion.Version20
            };

            if (headers != null)
                foreach (KeyValuePair<string, string> header in headers)
                    request.Headers.TryAddWithoutValidation(header.Key, header.Value);

            HttpResponseMessage response = await Util.HTTPUtil.AppHttpClient.SendAsync(request);
            byte[] bytes = await response.Content.ReadAsByteArrayAsync();

            return bytes;
        }
    }


    [JsonSerializable(typeof(AudioInfoWithCodecName))]
    [JsonSerializable(typeof(AudioInfoWitCodecId))]
    [JsonSerializable(typeof(DashJson))]
    [JsonSerializable(typeof(PlayViewReq))]
    [JsonSerializable(typeof(PlayViewReply))]
    [JsonSerializable(typeof(Dictionary<string, string>))]
    internal partial class JsonContext : JsonSerializerContext { }

    internal class AudioInfoWithCodecName
    {
        [JsonPropertyName("id")]
        public uint Id { get; }
        [JsonPropertyName("base_url")]
        public string BaseUrl { get; }
        [JsonPropertyName("backup_url")]
        public List<string> BackupUrl { get; }
        [JsonPropertyName("bandwidth")]
        public uint Bandwidth { get; }
        [JsonPropertyName("codecs")]
        public string Codecs { get; }

        public AudioInfoWithCodecName(uint id, string base_url,List<string> backup_url, uint bandwidth, string codecs)
        {
            Id = id;
            BaseUrl = base_url;
            BackupUrl = backup_url;
            Bandwidth = bandwidth;
            Codecs = codecs;
        }

        public override bool Equals(object? obj) => obj is AudioInfoWithCodecName other && Id == other.Id && BaseUrl == other.BaseUrl && BackupUrl.SequenceEqual(other.BackupUrl) && Bandwidth == other.Bandwidth && Codecs == other.Codecs;
        public override int GetHashCode() => HashCode.Combine(Id, BaseUrl, BackupUrl, Bandwidth, Codecs);
    }

    internal class AudioInfoWitCodecId
    {
        [JsonPropertyName("id")]
        public uint Id { get; }
        [JsonPropertyName("base_url")]
        public string BaseUrl { get; }
        [JsonPropertyName("bandwidth")]
        public uint Bandwidth { get; }
        [JsonPropertyName("codecid")]
        public uint Codecid { get; }

        public AudioInfoWitCodecId(uint id, string base_url, uint bandwidth, uint codecid)
        {
            Id = id;
            BaseUrl = base_url;
            Bandwidth = bandwidth;
            Codecid = codecid;
        }

        public override bool Equals(object? obj) => obj is AudioInfoWitCodecId other && Id == other.Id && BaseUrl == other.BaseUrl && Bandwidth == other.Bandwidth && Codecid == other.Codecid;
        public override int GetHashCode() => HashCode.Combine(Id, BaseUrl, Bandwidth, Codecid);
    }

    internal class DashInfo
    {
        [JsonPropertyName("video")]
        public List<object> Video { get; }
        [JsonPropertyName("audio")]
        public List<object> Audio { get; }

        public DashInfo(List<object> video, List<object> audio)
        {
            Video = video;
            Audio = audio;
        }

        public override bool Equals(object? obj) => obj is DashInfo other && EqualityComparer<List<object>>.Default.Equals(Video, other.Video) && EqualityComparer<List<object>>.Default.Equals(Audio, other.Audio);
        public override int GetHashCode() => HashCode.Combine(Video, Audio);
    }

    internal class DashData
    {
        [JsonPropertyName("timelength")]
        public ulong TimeLength { get; }
        [JsonPropertyName("dash")]
        public DashInfo Dash { get; }

        public DashData(ulong timelength, DashInfo dash)
        {
            TimeLength = timelength;
            Dash = dash;
        }

        public override bool Equals(object? obj) => obj is DashData other && TimeLength == other.TimeLength && EqualityComparer<DashInfo>.Default.Equals(Dash, other.Dash);
        public override int GetHashCode() => HashCode.Combine(TimeLength, Dash);
    }

    internal class DashJson
    {
        [JsonPropertyName("code")]
        public int Code { get; }
        [JsonPropertyName("message")]
        public string Message { get; }
        [JsonPropertyName("ttl")]
        public int Ttl { get; }
        [JsonPropertyName("data")]
        public DashData Data { get; }

        public DashJson(int code, string message, int ttl, DashData data)
        {
            Code = code;
            Message = message;
            Ttl = ttl;
            Data = data;
        }

        public override bool Equals(object? obj) => obj is DashJson other && Code == other.Code && Message == other.Message && Ttl == other.Ttl && EqualityComparer<DashData>.Default.Equals(Data, other.Data);
        public override int GetHashCode() => HashCode.Combine(Code, Message, Ttl, Data);
    }
}
